﻿using Arch.Core;
using Meow.Core.Simulation;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Runtime.Intrinsics.Arm;

namespace Meow.Core.Graphics;

public interface ICamera
{
    public BoundingFrustum CullingFrustum { get; }
    public void UpdateCullingFrustum();

    public void ClearEntitiesInSight();
    public void AddEntityInSight(in Entity entity);

    public List<Entity> GetEntitiesInSight();

    public Viewport Viewport { get; set; }
    public void UpdateAspectRatioWithViewport();

    public C_CameraRenderInfo GetRenderInfo();
}


/// <summary>
/// Simple yaw/pitch up-locked camera.
/// </summary>
public class SimpleCamera : ICamera
{
    /// <summary>
    /// Gets or sets the position of the camera.
    /// </summary>
    public Vector3 Position { get; set; }

    float yaw;
    /// <summary>
    /// Gets or sets the yaw of the camera as a value from -PI to PI. At 0, Forward is aligned with -z. At PI/2, Forward is aligned with +x. In other words, higher values turn right.
    /// </summary>
    public float Yaw
    {
        get { return yaw; }
        set
        {
            var revolution = (value + Math.PI) / (2 * Math.PI);
            revolution -= Math.Floor(revolution);
            yaw = (float)(revolution * (Math.PI * 2) - Math.PI);
        }
    }
    float pitch;
    /// <summary>
    /// Gets or sets the pitch of the camera, clamped to a value from -MaximumPitch to MaximumPitch. Higher values look downward, lower values look upward.
    /// </summary>
    public float Pitch
    {
        get { return pitch; }
        set { pitch = Math.Clamp(value, -maximumPitch, maximumPitch); }
    }

    float maximumPitch;
    /// <summary>
    /// Gets or sets the maximum pitch of the camera, a value from 0 to PI / 2.
    /// </summary>
    public float MaximumPitch
    {
        get { return maximumPitch; }
        set { maximumPitch = (float)Math.Clamp(value, 0, Math.PI / 2); }
    }

    /// <summary>
    /// Gets or sets the aspect ratio of the camera.
    /// </summary>
    public float AspectRatio { get; set; }

    /// <summary>
    /// Gets or sets the vertical field of view of the camera.
    /// </summary>
    public float FieldOfView { get; set; }

    /// <summary>
    /// Gets or sets the near plane of the camera.
    /// </summary>
    public float NearClip { get; set; }

    /// <summary>
    /// Gets or sets the far plane of the camera.
    /// </summary>
    public float FarClip { get; set; }

    //All of this could be quite a bit faster, but wasting a few thousand cycles per frame isn't exactly a concern.
    /// <summary>
    /// Gets the orientation quaternion of the camera.
    /// </summary>
    public Quaternion OrientationQuaternion
    {
        get
        {
            Quaternion.CreateFromYawPitchRoll(-yaw, -pitch, 0, out var orientationQuaternion);
            return orientationQuaternion;
        }
    }

    /// <summary>
    /// Gets the orientation transform of the camera.
    /// </summary>
    public Matrix Orientation
    {
        get
        {
            return Matrix.CreateFromQuaternion(OrientationQuaternion);
        }
    }

    /// <summary>
    /// Gets the right direction of the camera. Equivalent to transforming (1,0,0) by Orientation.
    /// </summary>
    public Vector3 Right
    {
        get
        {
            var orientation = OrientationQuaternion;
            orientation.TransformUnitX(out var right);
            return right;
        }
    }
    /// <summary>
    /// Gets the left direction of the camera. Equivalent to transforming (-1,0,0) by Orientation.
    /// </summary>
    public Vector3 Left
    {
        get
        {
            return -Right;
        }
    }
    /// <summary>
    /// Gets the up direction of the camera. Equivalent to transforming (0,1,0) by Orientation.
    /// </summary>
    public Vector3 Up
    {
        get
        {
            var orientation = OrientationQuaternion;
            orientation.TransformUnitY(out var up);
            return up;
        }
    }
    /// <summary>
    /// Gets the down direction of the camera. Equivalent to transforming (0,-1,0) by Orientation.
    /// </summary>
    public Vector3 Down
    {
        get
        {
            return -Up;
        }
    }
    /// <summary>
    /// Gets the backward direction of the camera. Equivalent to transforming (0,0,1) by Orientation.
    /// </summary>
    public Vector3 Backward
    {
        get
        {
            var orientation = OrientationQuaternion;
            orientation.TransformUnitZ(out var backward);
            return backward;
        }
    }
    /// <summary>
    /// Gets the forward direction of the camera. Equivalent to transforming (0,0,-1) by Orientation.
    /// </summary>
    public Vector3 Forward
    {
        get
        {
            return -Backward;
        }
    }

    /// <summary>
    /// Gets the world transform of the camera.
    /// </summary>
    public Matrix World
    {
        get
        {
            var world = Orientation;
            world.Translation = Position;
            return world;
        }
    }

    /// <summary>
    /// Gets the view transform of the camera.
    /// </summary>
    public Matrix View
    {
        get
        {
            return Matrix.Invert(World);
        }
    }

    /// <summary>
    /// Gets the projection transform of the camera using reversed depth.
    /// </summary>
    public Matrix Projection
    {
        get
        {
            //Note the flipped near/far! Reversed depth. Better precision distribution. Unlikely that we'll take advantage of it in the demos, but hey, it's free real estate.
            return Matrix.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearClip, FarClip);
        }
    }

    /// <summary>
    /// Gets the combined view * projection of the camera.
    /// </summary>
    public Matrix ViewProjection
    {
        get
        {
            return View * Projection;
        }
    }

    // Camera Culling
    BoundingFrustum cullingFrustum;

    /// <summary>
    /// Should Call UpdateCullingFrustum when the camera has been updated
    /// </summary>
    public BoundingFrustum CullingFrustum => cullingFrustum;

    public void UpdateCullingFrustum()
    {
        cullingFrustum = new BoundingFrustum(ViewProjection);
    }


    public readonly List<Entity> EntitiesInSight = new List<Entity>();
    public void ClearEntitiesInSight()
    {
        EntitiesInSight.Clear();
    }
    public void AddEntityInSight(in Entity entity)
    {
        EntitiesInSight.Add(entity);
    }

    public List<Entity> GetEntitiesInSight() => EntitiesInSight;

    public Viewport Viewport { get; set; }

    public void UpdateAspectRatioWithViewport()
    {
        AspectRatio = (float)Viewport.Width / (float)Viewport.Height;
    }

    public C_CameraRenderInfo GetRenderInfo() => new C_CameraRenderInfo()
    {
        PositionRender = Position,
        ViewRender = View,
        ProjectionRender = Projection,
    };

    /// <summary>
    /// Creates a new camera.
    /// </summary>
    /// <param name="aspectRatio">Aspect ratio of the camera's projection.</param>
    /// <param name="fieldOfView">Vertical field of view of the camera's projection.</param>
    /// <param name="nearClip">Near clip plane of the camera's projection.</param>
    /// <param name="farClip">Far clip plane of the camera's projection.</param>
    /// <param name="maximumPitch">Maximum angle that the camera can look up or down.</param>
    public SimpleCamera(float aspectRatio, float fieldOfView, float nearClip, float farClip, in Viewport viewport, float maximumPitch = MathF.PI * 0.499f)
    {
        AspectRatio = aspectRatio;
        FieldOfView = fieldOfView;
        MaximumPitch = maximumPitch;
        NearClip = nearClip;
        FarClip = farClip;
        Viewport = viewport;
    }

    public void LookAt(Vector3 target)
    {
        var dir = target - Position;
        var horizon = new Vector3(dir.X, 0, dir.Z);
        var forward = new Vector3(0f, 0f, -1f);
        Yaw = MathF.Acos(Vector3.Dot(forward, Vector3.Normalize(horizon))) * Math.Sign(-Vector3.Cross(forward, horizon).Y);
        Pitch = MathF.Atan(-dir.Y / horizon.Length());

    }

}
